Skip to main content

ScalingRule

ScalingRule enables pyramiding (scaling into winners) and partial exits (scaling out) on a per-symbol basis. Without it, every entry is a single full-size buy and every exit is a single full close.

from investing_algorithm_framework import ScalingRule

Signature

ScalingRule(
symbol: str | None = None,
max_entries: int = 1,
scale_in_percentage: float | list[float] = 100,
scale_out_percentage: float | list[float] = 50,
max_position_percentage: float | None = None,
cooldown_in_bars: int = 0,
)
ParameterTypeDefaultDescription
symbolstr | NoneNoneTarget symbol (e.g. "BTC"). When None, this entry is a default used for any symbol that doesn't have its own symbol-specific ScalingRule — a symbol-specific entry always takes precedence.
max_entriesint1Maximum total entries including the initial buy. 3 allows the initial entry plus 2 scale-ins.
scale_in_percentagefloat | list[float]100Size of each scale-in as a percent of the original PositionSize. Single value applies to all; list assigns per scale-in (last value reused if list is shorter).
scale_out_percentagefloat | list[float]50Percent of the current position to sell on each scale-out signal. Same single-vs-list semantics as scale_in_percentage.
max_position_percentagefloat | NoneNoneHard cap on total position size as percent of portfolio. Scale-ins that would breach the cap are reduced or skipped.
cooldown_in_barsint0Bars to wait after any buy/sell on this symbol before the next signal is acted on. Both-sides, symbol-scoped. For richer cooldowns use CooldownRule.

How Signals Map to Actions

When a ScalingRule is configured for a symbol, the engine treats incoming signals like this:

StateBuy signalSell signalScale-in signalScale-out signal
No positionOpen with full PositionSize(ignored, no position)(ignored)(ignored)
Has positionTreated as scale-inFull close, reset entry counterAdd scale_in_percentage[n] of original sizeSell scale_out_percentage[n] of current position

The full-sell signal always takes priority over a scale-out on the same bar.

Examples

Two-stage pyramid

scaling_rules = [
ScalingRule(
symbol="BTC",
max_entries=3, # 1 initial + 2 scale-ins
scale_in_percentage=[50, 25], # 1st add 50%, 2nd add 25%
),
]

With PositionSize(symbol="BTC", percentage_of_portfolio=20):

  • Initial buy: 20 % of portfolio.
  • 1st scale-in: 10 % (50 % of 20 %).
  • 2nd scale-in: 5 % (25 % of 20 %).
  • Total cap before max_position_percentage: 35 %.

Laddered partial exits

scaling_rules = [
ScalingRule(
symbol="ETH",
scale_out_percentage=[33, 50, 100],
),
]
  • 1st scale-out trims 33 % of the position.
  • 2nd scale-out trims 50 % of what's left.
  • 3rd scale-out (and any after) closes everything remaining.

Capped position with cooldown

scaling_rules = [
ScalingRule(
symbol="BTC",
max_entries=5,
scale_in_percentage=20,
max_position_percentage=40, # never exceed 40% of portfolio
cooldown_in_bars=4, # don't act on a new signal within 4 bars
),
]

Default for all symbols, with a per-symbol override

scaling_rules = [
# Applies to every traded symbol that doesn't have its own entry.
ScalingRule(max_entries=3, scale_in_percentage=[50, 25]),
# BTC gets a tighter cap instead of the default above.
ScalingRule(symbol="BTC", max_entries=2, max_position_percentage=30),
]

Generating Scale Signals

The strategy is responsible for producing scale-in / scale-out signals via:

def generate_scale_in_signals(self, data) -> dict[str, pd.Series]: ...
def generate_scale_out_signals(self, data) -> dict[str, pd.Series]: ...

If generate_scale_in_signals is not implemented, an extra buy signal on a symbol with an open position is treated as a scale-in (subject to max_entries).

Interaction With Other Rules

  • PositionSizescale_in_percentage is relative to the original PositionSize, so it does not drift as the portfolio grows.
  • CooldownRuleScalingRule.cooldown_in_bars is the legacy, both-sides, symbol-scoped cooldown. For side-aware or portfolio-wide throttling, use CooldownRule — both can coexist; the more restrictive one wins.
  • StopLossRule / TakeProfitRule — when they fire, sell_percentage is taken from the full current position, including all scaled-in lots.
  • TradingCost — fees and slippage apply to every scale-in and scale-out fill.

See Also